Skip to main content
Version: 2.0.1

☕ Space Java Client

Java client library for Space, a pricing-driven self-adaptation platform for SaaS applications.

Maven Central License: MIT

Table of Contents

🎁 What You Get

  • Simple API to connect to Space.
  • Contract lifecycle operations.
  • Feature evaluation with optional expected consumption.
  • Revert operation for optimistic usage updates.
  • Pricing token generation.
  • Built-in in-memory cache and Redis cache support.
  • WebSocket events for real-time pricing updates.

✅ Requirements

  • Java 21+
  • Maven 3.6+

📦 Installation

Add this dependency to your pom.xml:

<dependency>
<groupId>io.github.isa-group</groupId>
<artifactId>space-java-client</artifactId>
<version>{version}</version>
</dependency>

⚙️ SetUp

Quick Start in 2 Minutes (Spring)

1. Configure properties

Go to application.properties:

space.client.url=http://localhost:3000
space.client.api-key=your-api-key
space.client.timeout=10000

where:

  • space.client.url: URL of your SPACE instance (e.g., http://localhost:3000).
  • space.client.api-key: API key for authentication (obtainable from SPACE dashboard). It must be an organization-level API key.
  • space.client.timeout (default 10000): HTTP request timeout in milliseconds.

2. Scan and inject space-java-client's spring module

In your main application class, add the package scan for io.github.isagroup.spaceclient.spring:

package org.springframework.samples.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication()
@ComponentScan(basePackages = {
"org.springframework.samples.myapp",
"io.github.isagroup.spaceclient.spring" // ← Add this
})
public class MyApp {

public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}

}

3. Inject and use SpaceClient in a service

import io.github.isagroup.spaceclient.SpaceClient;
import io.github.isagroup.spaceclient.types.FeatureEvaluationResult;
import org.springframework.stereotype.Service;

@Service
public class PricingService {

private final SpaceClient spaceClient;

public PricingService(SpaceClient spaceClient) {
this.spaceClient = spaceClient;
}

public boolean canUseExport(String userId) {
FeatureEvaluationResult result = spaceClient.features.evaluate(userId, "myapp-billingExport");
return result.getError() == null && result.getEval();
}
}
warning

The example above is provided solely as a minimal illustration of how to bootstrap the usage of space-java-client within a SPACE-based application.

It assumes that a service named myapp is already provisioned in SPACE, along with an active contract associated with a user identified by userId. This user is subscribed to a specific service version in which the billingExport feature is explicitly governed by the pricing configuration.

As such, this example omits prerequisite steps such as service provisioning, contract creation, and feature configuration, all of which are required for correct evaluation and execution.

📚 API Overview

SpaceClientFactory

Factory utility for safe construction and input validation.

MethodDescription
connect(SpaceConnectionOptions options)Creates client with full options and validates required inputs.
connect(String url, String apiKey)Convenience overload, default timeout (5000).
connect(String url, String apiKey, int timeout)Convenience overload with custom timeout.

SpaceClient

Main entry point. Exposes modules as public fields:

  • contracts (ContractModule)
  • features (FeatureModule)
  • cache (CacheModule)

Core methods:

MethodReturnsDetails
isConnectedToSpace()booleanHTTP health check against /healthcheck.
on(String event, Consumer<Object> callback)voidRegisters event callback for supported event names.
removeListener(String event)voidRemoves one callback by event name.
removeAllListeners()voidRemoves all event callbacks.
connect()voidConnects/reconnects WebSocket namespace if disconnected.
disconnect()voidDisconnects WebSocket namespace and clears socket handlers.
close()voidCloses sockets, cache provider, and HTTP resources.

Getters:

  • getHttpUrl()
  • getApiKey()
  • getTimeout()
  • getObjectMapper()

ContractModule

Contract operations available at spaceClient.contracts.

MethodReturnsDetails
getContract(String userId)ContractReads from cache first when enabled; returns null on error.
addContract(ContractToCreate contractToCreate)ContractCreates contract and invalidates/refreshes user cache; null on error.
updateContractSubscription(String userId, Subscription newSubscription)ContractUpdates one user subscription; updates cache when enabled.
updateContractSubscriptionByGroupId(String groupId, Subscription newSubscription)List<Contract>Batch update by group ID; invalidates cache for each updated user.
updateContractUsageLevels(String userId, String serviceName, Map<String, Number> usageLevelsNovations)ContractUpdates usage levels for a user and service.
removeContract(String userId)voidDeletes contract and invalidates user cache if enabled.

Usage example:

import io.github.isagroup.spaceclient.types.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

UserContact userContact = new UserContact("user-123", "john_doe");
userContact.setEmail("john@example.com");

ContractToCreate.BillingPeriodToCreate billing =
new ContractToCreate.BillingPeriodToCreate(true, 30);

Map<String, String> contractedServices = Map.of("serviceA", "pricing/v1");
Map<String, String> subscriptionPlans = Map.of("serviceA", "basic");
Map<String, Map<String, Integer>> addOns = new HashMap<>();

ContractToCreate toCreate = new ContractToCreate(
userContact,
billing,
contractedServices,
"group-1",
subscriptionPlans,
addOns
);

Contract created = client.contracts.addContract(toCreate);
Contract current = client.contracts.getContract("user-123");

Subscription subscription = new Subscription(
contractedServices,
Map.of("serviceA", "premium"),
addOns
);

Contract updatedSingle = client.contracts.updateContractSubscription("user-123", subscription);
List<Contract> updatedGroup = client.contracts.updateContractSubscriptionByGroupId("group-1", subscription);

Map<String, Number> usagePatch = Map.of("requests", 120, "storage", 2048);
Contract usageUpdated = client.contracts.updateContractUsageLevels("user-123", "serviceA", usagePatch);

client.contracts.removeContract("user-123");

FeatureModule

Feature operations available at spaceClient.features.

MethodReturnsDetails
evaluate(String userId, String featureId)FeatureEvaluationResultRead-only evaluation. Cache can be used.
evaluate(String userId, String featureId, Map<String, Number> expectedConsumption)FeatureEvaluationResultEvaluation with consumption payload.
evaluate(String userId, String featureId, Map<String, Number> expectedConsumption, boolean details, boolean server)FeatureEvaluationResultFull control over query flags.
revertEvaluation(String userId, String featureId)booleanReverts evaluation update (latest behavior).
revertEvaluation(String userId, String featureId, boolean revertToLatest)booleanRevert strategy control (latest=true/false).
generateUserPricingToken(String userId)StringReturns token or null on failure.

Feature example:

import io.github.isagroup.spaceclient.types.FeatureEvaluationResult;
import java.util.HashMap;
import java.util.Map;

FeatureEvaluationResult readOnly = client.features.evaluate("user-123", "serviceA-featureX");
if (readOnly.getError() == null && readOnly.getEval()) {
System.out.println("Read-only evaluation enabled");
}

Map<String, Number> expectedConsumption = new HashMap<>();
expectedConsumption.put("requests", 10);
expectedConsumption.put("storage", 1024);

FeatureEvaluationResult withConsumption = client.features.evaluate(
"user-123",
"serviceA-featureX",
expectedConsumption,
true,
false
);

boolean reverted = client.features.revertEvaluation("user-123", "serviceA-featureX", true);
String pricingToken = client.features.generateUserPricingToken("user-123");

CacheModule

Cache module is exposed as spaceClient.cache. It is initialized automatically if enabled in SpaceConnectionOptions.

Common methods:

MethodDescription
isEnabled()Whether cache is active and provider initialized.
get(String key, Class<T> type)Read value by key.
set(String key, T value)Set value using default TTL from cache config.
set(String key, T value, Integer ttl)Set value with explicit TTL (seconds).
delete(String key)Delete one key.
has(String key)Check key presence.
clear()Clear all provider entries.
keys(String pattern)List keys by glob pattern.
invalidateUser(String userId)Clear common key groups for user.
close()Close provider resources.

Key helper methods:

  • getContractKey(userId)
  • getFeatureKey(userId, featureName)
  • getSubscriptionKey(userId)
  • getPricingTokenKey(userId)

📊 Data Models

SpaceConnectionOptions

Configuration options for establishing a connection to the Space server.

FieldTypeDefaultRequiredDescription
urlString-YesThe base URL of the Space server
apiKeyString-YesAPI key for authentication
timeoutInteger5000NoConnection timeout in milliseconds
cacheCacheOptionsnullNoCache configuration options

Example:

SpaceConnectionOptions options = new SpaceConnectionOptions();
options.setUrl("https://space.example.com");
options.setApiKey("sk-1234567890abcdef");
options.setTimeout(10000);

Or using the constructor:

SpaceConnectionOptions options = new SpaceConnectionOptions(
"https://space.example.com",
"sk-1234567890abcdef",
10000
);

CacheOptions

Cache configuration for storing feature evaluation results.

FieldTypeDefaultRequiredDescription
enabledbooleanfalseYesEnable/disable caching
typeCacheTypeBUILTINNoType of cache to use
ttlInteger300NoTime-to-live in seconds (default 5 minutes)
externalExternalCacheConfignullNoExternal cache configuration (required when type is REDIS)

CacheType Enum

ValueDescription
BUILTINIn-memory cache (default)
REDISExternal Redis cache

ExternalCacheConfig

FieldTypeDefaultRequiredDescription
redisRedisConfig-YesRedis server configuration

RedisConfig

FieldTypeDefaultRequiredDescription
hostString-YesRedis server hostname
portInteger6379NoRedis server port
passwordStringnullNoRedis authentication password
dbInteger0NoRedis database number (valid range: 0-15)
connectTimeoutInteger5000NoConnection timeout in milliseconds
keyPrefixString"space-client:"NoPrefix for cache keys

Examples:

Basic cache with built-in memory:

CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.BUILTIN);
cacheOptions.setTtl(600); // 10 minutes

Redis cache configuration:

CacheOptions.RedisConfig redisConfig = new CacheOptions.RedisConfig("redis.example.com");
redisConfig.setPort(6379);
redisConfig.setPassword("redis-password");
redisConfig.setDb(2);
redisConfig.setConnectTimeout(3000);
redisConfig.setKeyPrefix("petclinic:");

CacheOptions.ExternalCacheConfig externalConfig = new CacheOptions.ExternalCacheConfig();
externalConfig.setRedis(redisConfig);

CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.REDIS);
cacheOptions.setExternal(externalConfig);

FeatureEvaluationResult

Result of evaluating a feature flag against a user context.

FieldTypeDefaultRequiredDescription
evalboolean-YesWhether the feature is enabled
usedMap<String, Object>-NoVariables used in the evaluation
limitMap<String, Object>-NoLimit values applied
errorEvaluationErrornullNoError information if evaluation failed

EvaluationError

FieldTypeDefaultRequiredDescription
codeString-YesError code identifier
messageString-YesHuman-readable error message

Examples:

Successful evaluation:

FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(true);
result.setUsed(Map.of("userId", "user-123", "plan", "premium"));
result.setLimit(Map.of("maxRequests", 1000));
result.setError(null);

Evaluation with error:

FeatureEvaluationResult.EvaluationError error = new FeatureEvaluationResult.EvaluationError(
"FEATURE_NOT_FOUND",
"Feature 'dark-mode' does not exist"
);

FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(false);
result.setError(error);

Reading the result:

if (result.getError() != null) {
System.err.println("Error: " + result.getError().getCode() + " - " + result.getError().getMessage());
} else if (result.getEval()) {
System.out.println("Feature is enabled!");
// Access used variables
Object userId = result.getUsed().get("userId");
} else {
System.out.println("Feature is disabled");
}

ContractToCreate

Request object for creating a new contract.

FieldTypeDefaultRequiredDescription
userContactUserContact-YesContact information for the contract owner
billingPeriodBillingPeriodToCreate-YesBilling period configuration
groupIdStringnullNoGroup identifier
contractedServicesMap<String, String>-YesServices contracted (service name → service ID)
subscriptionPlansMap<String, String>-YesSubscription plans (plan name → plan ID)
subscriptionAddOnsMap<String, Map<String, Integer>>-YesAdd-ons with quantities (service → add-on → quantity)

BillingPeriodToCreate (nested class)

FieldTypeDefaultRequiredDescription
autoRenewBooleannullYesWhether the contract auto-renews
renewalDaysIntegernullYesDays before renewal to send notification

Example:

UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");

ContractToCreate.BillingPeriodToCreate billingPeriod = new ContractToCreate.BillingPeriodToCreate(
true, // autoRenew
30 // renewalDays
);

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5, "storage-gb", 10));

ContractToCreate contract = new ContractToCreate();
contract.setUserContact(contact);
contract.setBillingPeriod(billingPeriod);
contract.setGroupId("group-abc");
contract.setContractedServices(services);
contract.setSubscriptionPlans(plans);
contract.setSubscriptionAddOns(addons);

Contract

Full contract information retrieved from the Space server.

FieldTypeDefaultRequiredDescription
idString-NoContract identifier
_idString-NoInternal MongoDB identifier
userContactUserContact-YesContact information for the contract owner
billingPeriodBillingPeriod-YesCurrent billing period details
organizationIdString-NoOrganization identifier
groupIdString-NoGroup identifier
usageLevelsMap<String, Map<String, UsageLevel>>-YesUsage tracking per service and feature
contractedServicesMap<String, String>-YesServices contracted (service name → service ID)
subscriptionPlansMap<String, String>-YesSubscription plans (plan name → plan ID)
subscriptionAddOnsMap<String, Map<String, Integer>>-YesAdd-ons with quantities
historyList<ContractHistoryEntry>-YesHistory of contract changes

Example:

Contract contract = new Contract();
contract.setId("contract-001");
contract.setOrganizationId("org-petclinic");
contract.setGroupId("group-veterinary");

UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");
contract.setUserContact(contact);

BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(new Date());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(30);
contract.setBillingPeriod(billingPeriod);

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
contract.setContractedServices(services);

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");
contract.setSubscriptionPlans(plans);

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5));
contract.setSubscriptionAddOns(addons);

Map<String, Map<String, UsageLevel>> usageLevels = new HashMap<>();
UsageLevel level = new UsageLevel();
level.setConsumed(150.0);
level.setResetTimeStamp(new Date());
usageLevels.put("petclinic", Map.of("api-calls", level));
contract.setUsageLevels(usageLevels);

List<ContractHistoryEntry> history = new ArrayList<>();
// ... add history entries
contract.setHistory(history);

Subscription

Request object for updating an existing subscription.

FieldTypeDefaultRequiredDescription
contractedServicesMap<String, String>new HashMap<>()YesServices contracted (service name → service ID)
subscriptionPlansMap<String, String>new HashMap<>()YesSubscription plans (plan name → plan ID)
subscriptionAddOnsMap<String, Map<String, Integer>>new HashMap<>()YesAdd-ons with quantities

Example:

Subscription subscription = new Subscription();

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
subscription.setContractedServices(services);

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-enterprise");
subscription.setSubscriptionPlans(plans);

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 10, "storage-gb", 50));
subscription.setSubscriptionAddOns(addons);

Or using the constructor:

Subscription subscription = new Subscription(
Map.of("petclinic", "svc-petclinic-001"),
Map.of("petclinic", "plan-enterprise"),
Map.of("petclinic", Map.of("extra-users", 10))
);

BillingPeriod

Billing period information for a contract.

FieldTypeDefaultRequiredDescription
startDateDate-YesStart date of the billing period
endDateDate-YesEnd date of the billing period
autoRenewbooleanfalseYesWhether the contract auto-renews
renewalDaysint0YesDays before renewal to send notification

Example:

import java.util.Calendar;

Calendar startCal = Calendar.getInstance();
Calendar endCal = Calendar.getInstance();
endCal.add(Calendar.MONTH, 1);

BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(startCal.getTime());
billingPeriod.setEndDate(endCal.getTime());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(15);

Or using the constructor:

BillingPeriod billingPeriod = new BillingPeriod(
new Date(), // startDate
endDate, // endDate
true, // autoRenew
15 // renewalDays
);

UserContact

User contact information. Uses @JsonInclude(JsonInclude.Include.NON_NULL) to omit null fields during serialization.

FieldTypeDefaultRequiredDescription
userIdString-YesUser identifier
usernameString-YesUsername
firstNameString""NoFirst name (defaults to empty string to satisfy SPACE validation)
lastNameString""NoLast name (defaults to empty string to satisfy SPACE validation)
emailStringnullNoEmail address
phoneStringnullNoPhone number

Note: When using the constructor UserContact(userId, username), firstName and lastName are automatically set to empty strings to avoid SPACE validation errors.

Examples:

Minimal contact (recommended):

UserContact contact = new UserContact("user-001", "jdoe");
// firstName and lastName are automatically set to ""

Full contact:

UserContact contact = new UserContact(
"user-001",
"jdoe",
"John",
"Doe",
"john.doe@petclinic.com",
"+1-555-0123"
);

Using setters:

UserContact contact = new UserContact();
contact.setUserId("user-001");
contact.setUsername("jdoe");
contact.setFirstName("John");
contact.setLastName("Doe");
contact.setEmail("john.doe@petclinic.com");
contact.setPhone("+1-555-0123");

UsageLevel

Usage tracking information for a specific feature within a service.

FieldTypeDefaultRequiredDescription
resetTimeStampDate-YesWhen the usage counter will reset
consumeddouble0.0YesAmount of the limit consumed

Example:

UsageLevel usageLevel = new UsageLevel();
usageLevel.setConsumed(750.0);
usageLevel.setResetTimeStamp(new Date());

Or using the constructor:

UsageLevel usageLevel = new UsageLevel(resetDate, 750.0);

Checking usage against limits:

double consumed = usageLevel.getConsumed();
double limit = 1000.0;
double percentage = (consumed / limit) * 100;
System.out.println("Usage: " + percentage + "%");

ContractHistoryEntry

Historical record of a contract state.

FieldTypeDefaultRequiredDescription
startDateDate-YesStart date of this contract version
endDateDate-YesEnd date of this contract version
contractedServicesMap<String, String>-YesServices contracted during this period
subscriptionPlansMap<String, String>-YesSubscription plans during this period
subscriptionAddOnsMap<String, Map<String, Integer>>-YesAdd-ons with quantities during this period

Example:

ContractHistoryEntry entry = new ContractHistoryEntry();
entry.setStartDate(startDate);
entry.setEndDate(endDate);

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
entry.setContractedServices(services);

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-basic");
entry.setSubscriptionPlans(plans);

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 2));
entry.setSubscriptionAddOns(addons);

SpaceEvent

Enum representing events that can be listened to for real-time updates.

ValueEvent NameDescription
SYNCHRONIZED"synchronized"Data synchronization completed
PRICING_CREATED"pricing_created"New pricing was created
PRICING_ARCHIVED"pricing_archived"Pricing was archived
PRICING_ACTIVED"pricing_actived"Pricing was activated
SERVICE_DISABLED"service_disabled"A service was disabled
ERROR"error"An error occurred

Examples:

Using the enum directly:

SpaceEvent event = SpaceEvent.PRICING_CREATED;
String eventName = event.getEventName(); // "pricing_created"

Converting from string:

SpaceEvent event = SpaceEvent.fromString("synchronized");
if (event != null) {
System.out.println("Event: " + event.name());
}

Quick Reference: Map Key Conventions

Many types use Map<String, String> or Map<String, Map<String, Integer>>. Here are the typical key patterns:

contractedServices

Map<String, String> services = Map.of(
"petclinic", "svc-petclinic-001" // serviceName → serviceId
);

subscriptionPlans

Map<String, String> plans = Map.of(
"petclinic", "plan-premium" // serviceName → planId
);

subscriptionAddOns

Map<String, Map<String, Integer>> addons = Map.of(
"petclinic", Map.of( // serviceName → {addOnName → quantity}
"extra-users", 5,
"storage-gb", 10
)
);

usageLevels

Map<String, Map<String, UsageLevel>> usageLevels = Map.of(
"petclinic", Map.of( // serviceName → {featureName → UsageLevel}
"api-calls", new UsageLevel(resetDate, 150.0),
"storage", new UsageLevel(resetDate, 5.5)
)
);

CacheOptions

Cache configuration for storing feature evaluation results.

FieldTypeDefaultRequiredDescription
enabledbooleanfalseYesEnable/disable caching
typeCacheTypeBUILTINNoType of cache to use
ttlInteger300NoTime-to-live in seconds (default 5 minutes)
externalExternalCacheConfignullNoExternal cache configuration (required when type is REDIS)

CacheType Enum

ValueDescription
BUILTINIn-memory cache (default)
REDISExternal Redis cache

ExternalCacheConfig

FieldTypeDefaultRequiredDescription
redisRedisConfig-YesRedis server configuration

RedisConfig

FieldTypeDefaultRequiredDescription
hostString-YesRedis server hostname
portInteger6379NoRedis server port
passwordStringnullNoRedis authentication password
dbInteger0NoRedis database number (valid range: 0-15)
connectTimeoutInteger5000NoConnection timeout in milliseconds
keyPrefixString"space-client:"NoPrefix for cache keys

Examples

Basic cache with built-in memory:

CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.BUILTIN);
cacheOptions.setTtl(600); // 10 minutes

Redis cache configuration:

CacheOptions.RedisConfig redisConfig = new CacheOptions.RedisConfig("redis.example.com");
redisConfig.setPort(6379);
redisConfig.setPassword("redis-password");
redisConfig.setDb(2);
redisConfig.setConnectTimeout(3000);
redisConfig.setKeyPrefix("petclinic:");

CacheOptions.ExternalCacheConfig externalConfig = new CacheOptions.ExternalCacheConfig();
externalConfig.setRedis(redisConfig);

CacheOptions cacheOptions = new CacheOptions();
cacheOptions.setEnabled(true);
cacheOptions.setType(CacheOptions.CacheType.REDIS);
cacheOptions.setExternal(externalConfig);

FeatureEvaluationResult

Result of evaluating a feature flag against a user context.

FieldTypeDefaultRequiredDescription
evalboolean-YesWhether the feature is enabled
usedMap<String, Object>-NoVariables used in the evaluation
limitMap<String, Object>-NoLimit values applied
errorEvaluationErrornullNoError information if evaluation failed

EvaluationError

FieldTypeDefaultRequiredDescription
codeString-YesError code identifier
messageString-YesHuman-readable error message

Examples

Successful evaluation:

FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(true);
result.setUsed(Map.of("userId", "user-123", "plan", "premium"));
result.setLimit(Map.of("maxRequests", 1000));
result.setError(null);

Evaluation with error:

FeatureEvaluationResult.EvaluationError error = new FeatureEvaluationResult.EvaluationError(
"FEATURE_NOT_FOUND",
"Feature 'dark-mode' does not exist"
);

FeatureEvaluationResult result = new FeatureEvaluationResult();
result.setEval(false);
result.setError(error);

Reading the result:

if (result.getError() != null) {
System.err.println("Error: " + result.getError().getCode() + " - " + result.getError().getMessage());
} else if (result.getEval()) {
System.out.println("Feature is enabled!");
// Access used variables
Object userId = result.getUsed().get("userId");
} else {
System.out.println("Feature is disabled");
}

ContractToCreate

Request object for creating a new contract.

FieldTypeDefaultRequiredDescription
userContactUserContact-YesContact information for the contract owner
billingPeriodBillingPeriodToCreate-YesBilling period configuration
groupIdStringnullNoGroup identifier
contractedServicesMap<String, String>-YesServices contracted (service name → service ID)
subscriptionPlansMap<String, String>-YesSubscription plans (plan name → plan ID)
subscriptionAddOnsMap<String, Map<String, Integer>>-YesAdd-ons with quantities (service → add-on → quantity)

BillingPeriodToCreate (nested class)

FieldTypeDefaultRequiredDescription
autoRenewBooleannullYesWhether the contract auto-renews
renewalDaysIntegernullYesDays before renewal to send notification

Example

UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");

ContractToCreate.BillingPeriodToCreate billingPeriod = new ContractToCreate.BillingPeriodToCreate(
true, // autoRenew
30 // renewalDays
);

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5, "storage-gb", 10));

ContractToCreate contract = new ContractToCreate();
contract.setUserContact(contact);
contract.setBillingPeriod(billingPeriod);
contract.setGroupId("group-abc");
contract.setContractedServices(services);
contract.setSubscriptionPlans(plans);
contract.setSubscriptionAddOns(addons);

Contract

Full contract information retrieved from the Space server.

FieldTypeDefaultRequiredDescription
idString-NoContract identifier
_idString-NoInternal MongoDB identifier
userContactUserContact-YesContact information for the contract owner
billingPeriodBillingPeriod-YesCurrent billing period details
organizationIdString-NoOrganization identifier
groupIdString-NoGroup identifier
usageLevelsMap<String, Map<String, UsageLevel>>-YesUsage tracking per service and feature
contractedServicesMap<String, String>-YesServices contracted (service name → service ID)
subscriptionPlansMap<String, String>-YesSubscription plans (plan name → plan ID)
subscriptionAddOnsMap<String, Map<String, Integer>>-YesAdd-ons with quantities
historyList<ContractHistoryEntry>-YesHistory of contract changes

Example

Contract contract = new Contract();
contract.setId("contract-001");
contract.setOrganizationId("org-petclinic");
contract.setGroupId("group-veterinary");

UserContact contact = new UserContact("user-001", "jdoe", "John", "Doe", "john@example.com", "+1-555-0123");
contract.setUserContact(contact);

BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(new Date());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(30);
contract.setBillingPeriod(billingPeriod);

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
contract.setContractedServices(services);

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-premium");
contract.setSubscriptionPlans(plans);

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 5));
contract.setSubscriptionAddOns(addons);

Map<String, Map<String, UsageLevel>> usageLevels = new HashMap<>();
UsageLevel level = new UsageLevel();
level.setConsumed(150.0);
level.setResetTimeStamp(new Date());
usageLevels.put("petclinic", Map.of("api-calls", level));
contract.setUsageLevels(usageLevels);

List<ContractHistoryEntry> history = new ArrayList<>();
// ... add history entries
contract.setHistory(history);

Subscription

Request object for updating an existing subscription.

FieldTypeDefaultRequiredDescription
contractedServicesMap<String, String>new HashMap<>()YesServices contracted (service name → service ID)
subscriptionPlansMap<String, String>new HashMap<>()YesSubscription plans (plan name → plan ID)
subscriptionAddOnsMap<String, Map<String, Integer>>new HashMap<>()YesAdd-ons with quantities

Example

Subscription subscription = new Subscription();

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
subscription.setContractedServices(services);

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-enterprise");
subscription.setSubscriptionPlans(plans);

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 10, "storage-gb", 50));
subscription.setSubscriptionAddOns(addons);

Or using the constructor:

Subscription subscription = new Subscription(
Map.of("petclinic", "svc-petclinic-001"),
Map.of("petclinic", "plan-enterprise"),
Map.of("petclinic", Map.of("extra-users", 10))
);

BillingPeriod

Billing period information for a contract.

FieldTypeDefaultRequiredDescription
startDateDate-YesStart date of the billing period
endDateDate-YesEnd date of the billing period
autoRenewbooleanfalseYesWhether the contract auto-renews
renewalDaysint0YesDays before renewal to send notification

Example

import java.util.Calendar;

Calendar startCal = Calendar.getInstance();
Calendar endCal = Calendar.getInstance();
endCal.add(Calendar.MONTH, 1);

BillingPeriod billingPeriod = new BillingPeriod();
billingPeriod.setStartDate(startCal.getTime());
billingPeriod.setEndDate(endCal.getTime());
billingPeriod.setAutoRenew(true);
billingPeriod.setRenewalDays(15);

Or using the constructor:

BillingPeriod billingPeriod = new BillingPeriod(
new Date(), // startDate
endDate, // endDate
true, // autoRenew
15 // renewalDays
);

UserContact

User contact information. Uses @JsonInclude(JsonInclude.Include.NON_NULL) to omit null fields during serialization.

FieldTypeDefaultRequiredDescription
userIdString-YesUser identifier
usernameString-YesUsername
firstNameString""NoFirst name (defaults to empty string to satisfy SPACE validation)
lastNameString""NoLast name (defaults to empty string to satisfy SPACE validation)
emailStringnullNoEmail address
phoneStringnullNoPhone number

Note: When using the constructor UserContact(userId, username), firstName and lastName are automatically set to empty strings to avoid SPACE validation errors.

Examples

Minimal contact (recommended):

UserContact contact = new UserContact("user-001", "jdoe");
// firstName and lastName are automatically set to ""

Full contact:

UserContact contact = new UserContact(
"user-001",
"jdoe",
"John",
"Doe",
"john.doe@petclinic.com",
"+1-555-0123"
);

Using setters:

UserContact contact = new UserContact();
contact.setUserId("user-001");
contact.setUsername("jdoe");
contact.setFirstName("John");
contact.setLastName("Doe");
contact.setEmail("john.doe@petclinic.com");
contact.setPhone("+1-555-0123");

UsageLevel

Usage tracking information for a specific feature within a service.

FieldTypeDefaultRequiredDescription
resetTimeStampDate-YesWhen the usage counter will reset
consumeddouble0.0YesAmount of the limit consumed

Example

UsageLevel usageLevel = new UsageLevel();
usageLevel.setConsumed(750.0);
usageLevel.setResetTimeStamp(new Date());

Or using the constructor:

UsageLevel usageLevel = new UsageLevel(resetDate, 750.0);

Checking usage against limits:

double consumed = usageLevel.getConsumed();
double limit = 1000.0;
double percentage = (consumed / limit) * 100;
System.out.println("Usage: " + percentage + "%");

ContractHistoryEntry

Historical record of a contract state.

FieldTypeDefaultRequiredDescription
startDateDate-YesStart date of this contract version
endDateDate-YesEnd date of this contract version
contractedServicesMap<String, String>-YesServices contracted during this period
subscriptionPlansMap<String, String>-YesSubscription plans during this period
subscriptionAddOnsMap<String, Map<String, Integer>>-YesAdd-ons with quantities during this period

Example

ContractHistoryEntry entry = new ContractHistoryEntry();
entry.setStartDate(startDate);
entry.setEndDate(endDate);

Map<String, String> services = new HashMap<>();
services.put("petclinic", "svc-petclinic-001");
entry.setContractedServices(services);

Map<String, String> plans = new HashMap<>();
plans.put("petclinic", "plan-basic");
entry.setSubscriptionPlans(plans);

Map<String, Map<String, Integer>> addons = new HashMap<>();
addons.put("petclinic", Map.of("extra-users", 2));
entry.setSubscriptionAddOns(addons);

SpaceEvent

Enum representing events that can be listened to for real-time updates.

ValueEvent NameDescription
SYNCHRONIZED"synchronized"Data synchronization completed
PRICING_CREATED"pricing_created"New pricing was created
PRICING_ARCHIVED"pricing_archived"Pricing was archived
PRICING_ACTIVED"pricing_actived"Pricing was activated
SERVICE_DISABLED"service_disabled"A service was disabled
ERROR"error"An error occurred

Examples

Using the enum directly:

SpaceEvent event = SpaceEvent.PRICING_CREATED;
String eventName = event.getEventName(); // "pricing_created"

Converting from string:

SpaceEvent event = SpaceEvent.fromString("synchronized");
if (event != null) {
System.out.println("Event: " + event.name());
}

Quick Reference: Map Key Conventions

Many types use Map<String, String> or Map<String, Map<String, Integer>>. Here are the typical key patterns:

contractedServices

Map<String, String> services = Map.of(
"petclinic", "svc-petclinic-001" // serviceName → serviceId
);

subscriptionPlans

Map<String, String> plans = Map.of(
"petclinic", "plan-premium" // serviceName → planId
);

subscriptionAddOns

Map<String, Map<String, Integer>> addons = Map.of(
"petclinic", Map.of( // serviceName → {addOnName → quantity}
"extra-users", 5,
"storage-gb", 10
)
);

usageLevels

Map<String, Map<String, UsageLevel>> usageLevels = Map.of(
"petclinic", Map.of( // serviceName → {featureName → UsageLevel}
"api-calls", new UsageLevel(resetDate, 150.0),
"storage", new UsageLevel(resetDate, 5.5)
)
);

💾 Caching

The client supports two strategies.

1) Built-in in-memory cache

import io.github.isagroup.spaceclient.types.CacheOptions;
import io.github.isagroup.spaceclient.types.CacheOptions.CacheType;
import io.github.isagroup.spaceclient.types.SpaceConnectionOptions;

CacheOptions cacheOptions = new CacheOptions(true, CacheType.BUILTIN, 300);

SpaceConnectionOptions options = new SpaceConnectionOptions(
"http://localhost:3000",
"your-api-key",
5000,
cacheOptions
);

SpaceClient client = SpaceClientFactory.connect(options);

2) Redis cache

import io.github.isagroup.spaceclient.types.CacheOptions;
import io.github.isagroup.spaceclient.types.CacheOptions.CacheType;
import io.github.isagroup.spaceclient.types.CacheOptions.ExternalCacheConfig;
import io.github.isagroup.spaceclient.types.CacheOptions.RedisConfig;
import io.github.isagroup.spaceclient.types.SpaceConnectionOptions;

RedisConfig redis = new RedisConfig("localhost", 6379);
redis.setPassword("your-password"); // optional
redis.setDb(0);
redis.setConnectTimeout(5000);
redis.setKeyPrefix("space-client:");

ExternalCacheConfig external = new ExternalCacheConfig();
external.setRedis(redis);

CacheOptions cacheOptions = new CacheOptions(true, CacheType.REDIS, 300);
cacheOptions.setExternal(external);

SpaceConnectionOptions options = new SpaceConnectionOptions(
"http://localhost:3000",
"your-api-key",
5000,
cacheOptions
);

SpaceClient client = SpaceClientFactory.connect(options);

Cache notes:

  • Contract reads use cache when enabled.
  • Read-only feature evaluations may be cached for 60 seconds.
  • Pricing tokens may be cached for 900 seconds.
  • Mutations invalidate related user keys.

🔌 WebSocket Events

Supported events:

  • synchronized
  • pricing_created
  • pricing_archived
  • pricing_actived
  • service_disabled
  • error

Example:

client.on("synchronized", data -> System.out.println("Socket connected"));
client.on("pricing_created", data -> System.out.println("Pricing created: " + data));
client.on("error", err -> System.err.println("Socket error: " + err));

client.connect();

// ...

client.removeListener("pricing_created");
client.removeAllListeners();
client.disconnect();

⚠️ Error Handling

Current behavior by module:

  • SpaceClientFactory.connect(...) throws IllegalArgumentException for invalid input.
  • SpaceClient.isConnectedToSpace() returns false when health check fails.
  • ContractModule methods return null on HTTP/IO failure (except removeContract, which logs internally).
  • FeatureModule.evaluate(...) returns a FeatureEvaluationResult with error populated on IO failures.
  • FeatureModule.revertEvaluation(...) returns false on failure.
  • FeatureModule.generateUserPricingToken(...) returns null on failure.

Suggested defensive pattern:

FeatureEvaluationResult result = client.features.evaluate(userId, featureId);
if (result.getError() != null) {
// handle recoverable error
return;
}
if (!result.getEval()) {
// feature disabled
return;
}
// proceed

✨ Best Practices

  • Keep one SpaceClient per process/service where possible.
  • Always call client.close() on shutdown to release resources.
  • Use Redis cache for multi-instance deployments.
  • For deterministic behavior under network issues, always check null / false / error outputs.
  • Reuse connection options and avoid creating short-lived clients per request.

🛠️ Tech Stack

The project uses the following main tools and technologies:

Java Maven OkHttp Jackson Socket.IO Redis SLF4J JUnit 5 Mockito Spring

🤝 Contributing

Contributions are welcome. Open an issue or submit a pull request.

📄 Disclaimer & License

This project is licensed under the MIT License. See LICENSE.

warning

This SDK is part of ongoing research in pricing-driven devops. It is still in an early stage and not intended for production use.